Zwei Jahre CrowdSec, und jetzt mit WAF
Vor rund zwei Jahren habe ich auf unserem dedizierten Server
CrowdSec installiert – halb aus Neugier, halb weil mir
fail2ban nach Jahren treuer Dienste zu statisch geworden war. Inzwischen läuft
das Ding so unauffällig, dass ich fast vergessen hätte darüber zu schreiben.
Zeit für eine Zwischenbilanz.
Der Server beherbergt rund 70 Container in etwa 30 Docker-Compose-Projekten hinter einem einzigen Traefik – Nextcloud, GitLab, ein paar Kartendienste, diverse statische Seiten und einiges an Kleinkram. Also genau die Sorte Host, die im Internet permanent abgeklopft wird.
Was CrowdSec anders macht #
Der grundsätzliche Ablauf ist der von fail2ban: Logs lesen, Muster erkennen,
Angreifer sperren. Zwei Dinge sind anders. Erstens ist die Erkennung von der
Durchsetzung getrennt – der Agent wertet aus und trifft Entscheidungen, die
Bouncer setzen sie um; man kann also mehrere Durchsetzungspunkte an eine
Analyse hängen. Zweitens werden die Signale geteilt: Wer mitmacht, bekommt die
Community-Blocklist zurück und blockiert damit IP-Adressen, die anderswo schon
auffällig geworden sind, bevor sie es bei einem selbst versuchen.
Bei mir läuft der Agent bewusst auf dem Host und nicht im Container
(crowdsec v1.8.1 plus crowdsec-firewall-bouncer v0.0.36, beide als
systemd-Units). Der Agent muss ohnehin an das Docker-Log von Traefik und an
/var/log/auth.log heran, und ein Sicherheitswerkzeug, das mit dem
Container-Stack mitstirbt, den es schützen soll, fand ich keine gute Idee.
Zwei Ebenen der Durchsetzung #
Das ist der Teil, der mich anfangs am meisten Nachdenken gekostet hat, denn es gibt zwei Bouncer und sie tun sehr Unterschiedliches:
- Der Firewall-Bouncer wirft Pakete auf nftables-Ebene für den ganzen Host weg. Grobes Werkzeug, aber billig: Aktuell stehen dort 23.504 IP-Adressen, wovon 23.475 aus der Community-Blocklist stammen und nur 29 lokal erzeugt wurden. Gemeinsam haben sie 905.420 Pakete bzw. 58,8 MB verworfen.
- Das Traefik-Plugin
(
maxlerebourg/crowdsec-bouncer-traefik-plugin) entscheidet pro HTTP-Request im Proxy, bevor das Backend überhaupt etwas mitbekommt. Eine so geblockte Anfrage erkennt man im Traefik-Access-Log gut daran, dassServiceNameleer bleibt.
Das Verhältnis 23.475 zu 29 finde ich bemerkenswert: Der weitaus größte Teil dessen, was hier abgewehrt wird, hat nie bei mir angeklopft, sondern wurde von jemand anderem gemeldet. Genau dafür teilt man die Signale.
Der neue Teil: AppSec als WAF #
Wirklich interessant wurde es aber erst mit dem AppSec-Modul, das CrowdSec zu einer Web Application Firewall macht. Der Unterschied zur bisherigen Betrachtung ist grundlegend: Bisher wurden Angreifer im Nachhinein anhand ihres Log-Verhaltens erkannt – ein Scanner musste erst ein paar Mal danebengreifen, bevor er aufflog. AppSec prüft den Request selbst, bevor er durchgelassen wird.
Die Engine hört auf 0.0.0.0:7422 und wird in /etc/crowdsec/acquis.d/
deklariert. Der spannendste Regelsatz heißt
crowdsecurity/virtual-patching:
Regeln für konkrete, bekannte CVEs in verbreiteter Software. Der Name ist gut
gewählt – man patcht damit nicht die Applikation, sondern schiebt dem bekannten
Exploit-Pfad einen Riegel vor. Die Sammlung wächst dabei laufend mit: Aktuell
stehen 195 Regeln dahinter, jede für eine eigene CVE – von altbekannten
Klassikern bis zu Einträgen mit einer CVE-Nummer aus 2026.
Eingebunden wird das im Traefik-Plugin, das dieselbe Middleware sowohl für die LAPI-Abfrage als auch für AppSec verwendet:
[http.middlewares.crowdsec.plugin.crowdsec]
enabled = true
logLevel = "INFO"
crowdsecMode = "live"
crowdsecLapiScheme = "http"
crowdsecLapiHost = "host.docker.internal:8080"
crowdsecLapiKey = "<hier steht der LAPI-Key>"
crowdsecAppsecEnabled = true
crowdsecAppsecHost = "host.docker.internal:7422"
crowdsecAppsecFailureBlock = true
crowdsecAppsecUnreachableBlock = false
Die letzten beiden Zeilen sind die eigentliche Design-Entscheidung.
crowdsecAppsecFailureBlock = true heißt: Trifft eine Regel zu, wird der
Request sofort geblockt. crowdsecAppsecUnreachableBlock = false heißt: Ist die
AppSec-Engine nicht erreichbar, geht der Request trotzdem durch. Man wählt hier
zwischen “im Zweifel dicht” und “im Zweifel offen”, und ich habe mich bewusst
für Verfügbarkeit entschieden – ein abgestürzter Sicherheitsdienst soll nicht
alle 30 Applikationen offline nehmen.
Das hat allerdings einen Haken, über den man Bescheid wissen muss: Wenn AppSec stirbt, merkt das niemand. Die Seiten funktionieren ja weiter, nur eben ungefiltert. Bei mir hängt deshalb ein Prometheus-Job an CrowdSec, der genau dafür da ist.
Eine Ausnahme mache ich bewusst: maps.webman.at,
meine
Karte auf Basis von PMTiles, hängt nicht hinter
dem CrowdSec-Plugin. Ursprünglich hatte ich die WAF für alle rund 30
Compose-Projekte auf einmal an einer Stelle in der Traefik-Konfiguration
aktiviert – das sah zunächst elegant aus, brachte aber prompt die Karte zum
Stillstand: Tiles wurden sehr stark verzögert und zum Teil gar nicht mehr
geladen. Bei der Anfragefrequenz einer Kartenanwendung – schon ein einziger
Zoom- oder Pan-Vorgang löst Dutzende bis Hunderte Tile-Requests aus – reicht
die zusätzliche Latenz der AppSec-Prüfung pro Request, um die Karte praktisch
unbenutzbar zu machen. Seither bekommt der Router von maps.webman.at bewusst
kein crowdsec@file-Middleware mehr zugewiesen. Der Firewall-Bouncer greift
dort trotzdem weiter, weil der auf nftables-Ebene ohnehin für den ganzen Host
gilt und nichts mit einzelnen Requests zu tun hat.
Was in zwei Jahren angefallen ist #
Ein Blick auf cscli metrics in einem typischen Zeitfenster:
| Quelle | Zeilen gelesen | geparst | in Buckets |
|---|---|---|---|
docker:traefik-traefik-1 |
393.675 | 393.621 (99,99 %) | 29.396 |
file:/var/log/auth.log |
31.140 | 2.264 | 125 |
appsec:appsec |
2.419 | 2.419 | 616 |
Die häufigsten lokal ausgelösten Szenarien sind fast schon langweilig
vorhersagbar: http-probing (9.200 Mal), http-sensitive-files (3.572),
vpatch-env-access (1.535), vpatch-git-config (212), http-bad-user-agent
(153). Übersetzt: Es wird pausenlos nach vergessenen .env-Dateien und offen
liegenden .git-Verzeichnissen gesucht. Nichts Kreatives – aber es reicht ja,
wenn es einmal klappt.
Auf den echten Traffic gerechnet bekommen etwa 0,2 % der Anfragen ein 403.
Stichproben zeigen dahinter Crawler (Bytespider, Amazonbot, Amzn-SearchBot) oder
klar bösartige Pfade wie /.env, /_profiler/phpinfo oder
/?rest_route=/batch/v1. Blockierten Browser-Traffic von echten Besucher*innen
habe ich bisher keinen gefunden – was mir bei einem WAF die wichtigste Zahl
überhaupt ist.
Stolpersteine #
Ganz ohne Kanten war es nicht:
- Whitelists nicht vergessen. Die Collection
whitelist-good-actorsbringt CDN-, SEO-Bot- und Google-Crawler-Whitelists mit. Ohne so etwas sperrt man sich früher oder später den eigenen Uptime-Check oder einen Suchmaschinen-Bot aus. - Der Traefik-Bouncer registriert sich unter seiner Container-IP
(
traefik-bouncer@<ip>). Bei jedem IP-Wechsel entsteht ein neuer Eintrag; bei mir haben sich inzwischen sechs angesammelt, von denen weiterhin genau einer tatsächlich Daten abholt. Harmlos, aber verwirrend – aufräumen mitcscli bouncers delete traefik-bouncer@<ip>. - Der LAPI-Key steht im Klartext in der Traefik-Konfiguration. Wer diese Datei, so wie ich, in Git versioniert, sollte sich das vorher überlegen und nicht, so wie ich, hinterher 🙈.
Fazit #
Nach zwei Jahren ist mein Urteil deutlich: Der Firewall-Bouncer mit der
Community-Blocklist ist für den Aufwand von einer halben Stunde Installation ein
absurd gutes Geschäft. AppSec ist die interessantere Ergänzung, weil es nicht
mehr nur den Absender bewertet, sondern die Anfrage selbst – und virtual-patching
ist genau das, was man auf einem Host mit dreißig gewachsenen Applikationen
haben will, von denen nicht jede jederzeit auf dem letzten Stand ist.
Was ich beim nächsten Mal anders machen würde: gleich zu Beginn das Monitoring aufsetzen. Ein Sicherheitswerkzeug, das leise ausfällt, ist schlimmer als keines, weil man sich in Sicherheit wähnt.